Skip to content

Conversation

@LiGaCu
Copy link
Contributor

@LiGaCu LiGaCu commented May 15, 2025

Problem

For server-side workspace context capability, we would like to consolidate all the workspace folders under the same IDE workspace into a single remote workspace so that we could provide better workspace-level context for cross-workspace-folder scenarios.

Prerequisite changes have been made:

Solution

Updated the fundamental logic of workspaceContext/workspaceFolderManager to map all the events into a single remote workspace. Modified workspaceContext/workspaceContextServer logic to accept the workspace identifier passed from extensions before kicking off the workflow. Modified DependencyDiscoverer, LanguageDependencyHandler, ArtifactManager, etc. to accommodate the logic updates.

Performed E2E testing and verified the logic works well.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@LiGaCu LiGaCu requested a review from a team as a code owner May 15, 2025 21:53
@LiGaCu LiGaCu changed the title One remote workspace feat: launch one remote workspace for all workspace folders May 15, 2025
private workspaceFolders: WorkspaceFolder[]
public dependencyHandlerRegistry: LanguageDependencyHandler<BaseDependencyInfo>[] = []
private initializedWorkspaceFolder = new Map<WorkspaceFolder, boolean>()
// Create a SharedArrayBuffer with 4 bytes (for a 32-bit unsigned integer) for thread-safe counter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for posterity, I believe we don't need to go with this approach since we are not using any workers or threads in the code and everything is single threaded at the moment

a simple data type like the following would also work here

type DependencySizeTracker = {
    totalSize: number
    maxSize: number
}

const sizeTracker: DependencySizeTracker = {
    totalSize: 0,
    maxSize: 8 * 1024 * 1024 * 1024
}

The current approach is still fine and I am not suggesting we change it.

dispose(): void {
this.dependencyMap.clear()
this.dependencyUploadedSize.clear()
Atomics.store(this.dependencyUploadedSizeSum, 0, 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done once inside the dispose function of the dependencyDiscoverer instead of being done inside each individual dependency handler multiple times

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense, will move it.

}, this.CONTINUOUS_MONITOR_INTERVAL)
// Otherwise, wait for the promise to resolve or catch the rejection and retry
try {
return await this.remoteWorkspaceIdPromise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when there is a client side issue like user credentials expired or some backend problem, wouldn't this promise just keep getting rejected over and over and wouldn't it cause a continuous loop of waitForRemoteWorkspaceId being called with no exit

Is there some other exit scenario here that I am maybe missing?

Copy link
Contributor

@ege0zcan ege0zcan May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The waitForRemoteWorkspaceId function is currently being called inside an interval right now from initializeWorkspaceStatusMonitor

If the waitForRemoteWorkspaceId call goes into an infinite cycle, new intervals would start getting created before the previous one resolves. So the number of active intervals would linearly increase since none of the open intervals would resolve. This could lead to very unpleasant UX for the unhappy paths

Edit: I misread the code so the first part of this comment is wrong and should be ignored.

The function waits until the promise resolves (and it can possibly never resolve because we never get a workspace id) so it should be used carefully, but the suggested usages in this PR look good to me. In the future we can consider giving up on waiting the promises after a certain period of time has passed as an improvement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The waitForRemoteWorkspaceId function is currently being called inside an interval right now from initializeWorkspaceStatusMonitor

Not really, neither initializeWorkspaceStatusMonitor nor checkRemoteWorkspaceStatusAndReact would call waitForRemoteWorkspaceId. So, none of these intervals would get blocked by remoteWorkspaceIdPromise and no loop would be formed.

this.workspaceState.webSocketClient
.send(message)
.then(() => {
this.logging.log(` Message sent successfully`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styling: unnecessary space before the log

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also the log is somewhat ambiguous, would be helpful to at least specify that this is a websocket message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will modify this log.

})
}
}
}, this.MESSAGE_PUBLISH_INTERVAL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we now have the websocket message frequency being controlled by the workspace folder manager, I believe we should be able to simplify and remove the wrong throttling that we have on the websocket client

We should remove this line -> https://github.com/aws/language-servers/blob/main/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/client.ts#L159 and make the send function synchronous. It will also help simplify rest of the code where send was being called

Line 159 linked above is not useful even now since it applies a 100 ms delay to every websocket send call. So if send is called with 5 different messages at the same time, all the calls will wait 100 ms and they will all be sent at the same time meaning that the existing delay is actually completely redundant.

The scenario explained in the comment https://github.com/aws/language-servers/blob/main/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/client.ts#L144-L156 is too extreme of an example and as explained in the paragraph above, the artificial delay is redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense, will remove the wait within WebSocketClient

this.logging.error(`Error sending didChangeWorkspaceFolders message: ${e}`)
})
}
websocketClient.disconnect()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any case where we would want to disconnect the websocket client to be more efficient with resource usage? For example if the user removes all workspace folders from their workspace. If the user adds a new workspace folder after this happens, the code should probably be able to re-open a new ws connection

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, those are the improvements we could consider separately.

workspaceFolders: WorkspaceFolder[],
credentialsProvider: CredentialsProvider
credentialsProvider: CredentialsProvider,
workspaceIdentifier: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client-side generated workspaceIdentifier are not supposed to be used by others (like inline completions), they would need to fetch the server-side generated workspaceState.workspaceId and use that in API requests.

I would say we can add an interface separately from this PR.

},
})

// We add this event to the front of the queue here to prevent any race condition that might put events before the didChangeWorkspaceFolders event
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment can be removed from here now that we are no longer adding the message to the front of the queue anymore. This was a best effort mechanism to decrease the likelihood of sending other messages before the didChangeWorkspaceFolders message but it is not a strong mechanism.

As far as I can see it is still possible that if the ws connection gets established before than the source code preparation is finished, any LSP event like didSave event can be sent before the didChangeWorkspaceFolders event.

Maybe we should add a flag to the websocket publishing logic in workspace folder manager to keep track of the initial didChangeWorkspaceFolders being sent and until the initial didChangeWorkspaceFolders event(s) is/are sent, the websocket messages would be queued. Not suggesting to do it in this PR but would be a nice followup

leigaol pushed a commit to aws/aws-toolkit-vscode that referenced this pull request May 19, 2025
## Problem

In order to coordinate the new
aws/language-servers#1348 change,
GenerateCompletions requests would start expecting the `fileUri` field
to be set.

## Solution

Add `fileUri` to FileContext

---

- Treat all work as PUBLIC. Private `feature/x` branches will not be
squash-merged at release time.
- Your code changes must meet the guidelines in
[CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines).
- License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.

---------

Co-authored-by: Jiatong Li <[email protected]>
if (this.ws?.readyState === WebSocket.OPEN) {
await new Promise(resolve => setTimeout(resolve, this.messageThrottleDelay))
this.ws.send(message)
this.logging.debug('Message sent successfully to the remote workspace')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder that debug logs are disabled by default on production

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, not seeing these messages valuable to present in user logs.

@ege0zcan ege0zcan merged commit c240997 into aws:main May 19, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants